/*-------------------------------------------------------------------------
 *
 * plancat.c
 *	   访问系统目录的例程
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/util/plancat.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "access/genam.h"
#include "access/htup_details.h"
#include "access/nbtree.h"
#include "access/sysattr.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xlog.h"
#include "catalog/catalog.h"
#include "catalog/heap.h"
#include "catalog/pg_am.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/supportnodes.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/plancat.h"
#include "optimizer/prep.h"
#include "parser/parse_relation.h"
#include "parser/parsetree.h"
#include "partitioning/partdesc.h"
#include "rewrite/rewriteManip.h"
#include "statistics/statistics.h"
#include "storage/bufmgr.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/partcache.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"

/* GUC 参数 */
int			constraint_exclusion = CONSTRAINT_EXCLUSION_PARTITION;

/* 插件在 get_relation_info() 中获取控制权的钩子 */
get_relation_info_hook_type get_relation_info_hook = NULL;


static void get_relation_foreign_keys(PlannerInfo *fc_root, RelOptInfo *fc_rel,
									  Relation fc_relation, bool fc_inhparent);
static bool fc_infer_collation_opclass_match(InferenceElem *fc_elem, Relation fc_idxRel,
										  List *fc_idxExprs);
static List *get_relation_constraints(PlannerInfo *fc_root,
									  Oid fc_relationObjectId, RelOptInfo *fc_rel,
									  bool fc_include_noinherit,
									  bool fc_include_notnull,
									  bool fc_include_partition);
static List *fc_build_index_tlist(PlannerInfo *fc_root, IndexOptInfo *fc_index,
							   Relation fc_heapRelation);
static List *get_relation_statistics(RelOptInfo *fc_rel, Relation fc_relation);
static void set_relation_partition_info(PlannerInfo *fc_root, RelOptInfo *fc_rel,
										Relation fc_relation);
static PartitionScheme fc_find_partition_scheme(PlannerInfo *fc_root, Relation fc_rel);
static void set_baserel_partition_key_exprs(Relation fc_relation,
											RelOptInfo *fc_rel);
static void set_baserel_partition_constraint(Relation fc_relation,
											 RelOptInfo *fc_rel);



/*
 * get_relation_info -
 *	  检索给定关系的目录信息。
 *
 * 根据关系的 Oid，返回以下信息到 RelOptInfo 结构的字段中：
 *
 *	min_attr	最低有效的 AttrNumber
 *	max_attr	最高有效的 AttrNumber
 *	indexlist	关系索引的 IndexOptInfos 列表
 *	statlist	关系统计对象的 StatisticExtInfo 列表
 *	serverid	如果是外部表，则为服务器 OID
 *	fdwroutine	如果是外部表，则为 FDW 函数指针
 *	pages		页面数量
 *	tuples		元组数量
 *	rel_parallel_workers 用户定义的并行工作者数量
 *
 * 此外，添加关于关系外键的信息到 root->fkey_list。
 *
 * 还要初始化 attr_needed[] 和 attr_widths[] 数组。在大多数情况下这些被留作零，但有时我们需要在这里计算 attr 宽度，我们可以为 costsize.c 缓存结果。
 *
 * 如果 inhparent 为真，我们所需要做的就是设置 attr 数组：
 * RelOptInfo 实际上代表由继承树形成的 appendrel，因此，父关系的物理大小和索引信息对它并不重要。
 */
void get_relation_info(PlannerInfo *fc_root, Oid fc_relationObjectId, bool fc_inhparent,
				  RelOptInfo *fc_rel)
{
	Index		fc_varno = fc_rel->relid;
	Relation	fc_relation;
	bool		fc_hasindex;
	List	   *fc_indexinfos = NIL;

	/*
	 * 我们无需锁定关系，因为它已经被锁定，要么是通过重写器，要么在 expand_inherited_rtentry() 中将其添加到查询的 rangetable 中。
	 */
	fc_relation = table_open(fc_relationObjectId, NoLock);

	/*
	 * 只有在它们是特殊处理的 relkind 时，无表 AM 的关系才可以在查询中使用。这个检查防止我们在以后的过程中崩溃，比如，一个视图的 ON SELECT 规则缺失了。请注意，table_open() 已经拒绝了索引和复合类型；将错误以相同的方式拼写。
	 */
	if (!fc_relation->rd_tableam)
	{
		if (!(fc_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
			  fc_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("cannot open relation \"%s\"",
							RelationGetRelationName(fc_relation)),
					 errdetail_relkind_not_supported(fc_relation->rd_rel->relkind)));
	}

	/* 临时和未记录的关系在恢复期间无法访问。 */
	if (!RelationIsPermanent(fc_relation) && RecoveryInProgress())
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot access temporary or unlogged relations during recovery")));

	fc_rel->min_attr = FirstLowInvalidHeapAttributeNumber + 1;
	fc_rel->max_attr = RelationGetNumberOfAttributes(fc_relation);
	fc_rel->reltablespace = RelationGetForm(fc_relation)->reltablespace;

	Assert(fc_rel->max_attr >= fc_rel->min_attr);
	fc_rel->attr_needed = (Relids *)
		palloc0((fc_rel->max_attr - fc_rel->min_attr + 1) * sizeof(Relids));
	fc_rel->attr_widths = (int32 *)
		palloc0((fc_rel->max_attr - fc_rel->min_attr + 1) * sizeof(int32));

	/*
	 * 估算关系大小 --- 除非它是继承父关系，在这种情况下，我们想要的大小不是关系自己的大小，而是其继承树的大小。那将在 set_append_rel_size() 中计算。
	 */
	if (!fc_inhparent)
		estimate_rel_size(fc_relation, fc_rel->attr_widths - fc_rel->min_attr,
						  &fc_rel->pages, &fc_rel->tuples, &fc_rel->allvisfrac);

	/* 检索 parallel_workers reloption，如果未设置则为 -1。 */
	fc_rel->rel_parallel_workers = RelationGetParallelWorkers(fc_relation, -1);

	/*
	 * 制作索引列表。如果被告知，则忽略系统目录上的索引。
	 * 对于继承父关系的索引也不必费心。
	 */
	if (fc_inhparent ||
		(IgnoreSystemIndexes && IsSystemRelation(fc_relation)))
		fc_hasindex = false;
	else
		fc_hasindex = fc_relation->rd_rel->relhasindex;

	if (fc_hasindex)
	{
		List	   *fc_indexoidlist;
		LOCKMODE	fc_lmode;
		ListCell   *fc_l;

		fc_indexoidlist = RelationGetIndexList(fc_relation);

		/*
		 * 对于每个索引，我们获得执行者需要的相同类型的锁，并且不释放它。这节省了几次前往共享锁管理器的行程，同时没有造成实际的并发损失，因为在我们持有父关系的锁时，索引上不会发生任何架构更改，并且用于查询的锁类型不会阻碍任何其他类型的索引操作。
		 */
		fc_lmode = fc_root->simple_rte_array[fc_varno]->rellockmode;

		foreach(fc_l, fc_indexoidlist)
		{
			Oid			fc_indexoid = lfirst_oid(fc_l);
			Relation	fc_indexRelation;
			Form_pg_index fc_index;
			IndexAmRoutine *fc_amroutine;
			IndexOptInfo *fc_info;
			int			fc_ncolumns,
						fc_nkeycolumns;
			int			fc_i;

			/*
			 * 从关系描述符中提取索引信息。
			 */
			fc_indexRelation = index_open(fc_indexoid, fc_lmode);
			fc_index = fc_indexRelation->rd_index;

			/*
			 * 忽略无效索引，因为它们不能安全地用于查询。请注意，这没有问题，因为我们正在构建的数据结构仅供规划器使用 --- 执行者仍然需要插入“无效”索引，只要它们被标记为 indisready。
			 */
			if (!fc_index->indisvalid)
			{
				index_close(fc_indexRelation, NoLock);
				continue;
			}

			/*
			 * 忽略分区索引，因为它们不能用于查询。
			 */
			if (fc_indexRelation->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
			{
				index_close(fc_indexRelation, NoLock);
				continue;
			}

			/*
			 * 如果索引是有效的，但尚不可用，则忽略它；但将我们生成的计划标记为暂时。有关讨论，请参见 src/backend/access/heap/README.HOT。
			 */
			if (fc_index->indcheckxmin &&
				!TransactionIdPrecedes(HeapTupleHeaderGetXmin(fc_indexRelation->rd_indextuple->t_data),
									   TransactionXmin))
			{
				fc_root->glob->transientPlan = true;
				index_close(fc_indexRelation, NoLock);
				continue;
			}

			fc_info = makeNode(IndexOptInfo);

			fc_info->indexoid = fc_index->indexrelid;
			fc_info->reltablespace =
				RelationGetForm(fc_indexRelation)->reltablespace;
			fc_info->rel = fc_rel;
			fc_info->ncolumns = fc_ncolumns = fc_index->indnatts;
			fc_info->nkeycolumns = fc_nkeycolumns = fc_index->indnkeyatts;

			fc_info->indexkeys = (int *) palloc(sizeof(int) * fc_ncolumns);
			fc_info->indexcollations = (Oid *) palloc(sizeof(Oid) * fc_nkeycolumns);
			fc_info->opfamily = (Oid *) palloc(sizeof(Oid) * fc_nkeycolumns);
			fc_info->opcintype = (Oid *) palloc(sizeof(Oid) * fc_nkeycolumns);
			fc_info->canreturn = (bool *) palloc(sizeof(bool) * fc_ncolumns);

			for (fc_i = 0; fc_i < fc_ncolumns; fc_i++)
			{
				fc_info->indexkeys[fc_i] = fc_index->indkey.values[fc_i];
				fc_info->canreturn[fc_i] = index_can_return(fc_indexRelation, fc_i + 1);
			}

			for (fc_i = 0; fc_i < fc_nkeycolumns; fc_i++)
			{
				fc_info->opfamily[fc_i] = fc_indexRelation->rd_opfamily[fc_i];
				fc_info->opcintype[fc_i] = fc_indexRelation->rd_opcintype[fc_i];
				fc_info->indexcollations[fc_i] = fc_indexRelation->rd_indcollation[fc_i];
			}

			fc_info->relam = fc_indexRelation->rd_rel->relam;

			/* 我们只复制所需的字段，而不是 rd_indam 的所有字段 */
			fc_amroutine = fc_indexRelation->rd_indam;
			fc_info->amcanorderbyop = fc_amroutine->amcanorderbyop;
			fc_info->amoptionalkey = fc_amroutine->amoptionalkey;
			fc_info->amsearcharray = fc_amroutine->amsearcharray;
			fc_info->amsearchnulls = fc_amroutine->amsearchnulls;
			fc_info->amcanparallel = fc_amroutine->amcanparallel;
			fc_info->amhasgettuple = (fc_amroutine->amgettuple != NULL);
			fc_info->amhasgetbitmap = fc_amroutine->amgetbitmap != NULL &&
				fc_relation->rd_tableam->scan_bitmap_next_block != NULL;
			fc_info->amcanmarkpos = (fc_amroutine->ammarkpos != NULL &&
								  fc_amroutine->amrestrpos != NULL);
			fc_info->amcostestimate = fc_amroutine->amcostestimate;
			Assert(fc_info->amcostestimate != NULL);

			/* 获取索引操作类选项 */
			fc_info->opclassoptions = RelationGetIndexAttOptions(fc_indexRelation, true);

			/*
			 * 如果有的话，获取索引的排序信息。
			 */
			if (fc_info->relam == BTREE_AM_OID)
			{
				/*
				 * 如果是 btree 索引，我们可以直接使用其 opfamily OIDs
				 * 作为排序顺序的 opfamily OIDs。
				 */
				Assert(fc_amroutine->amcanorder);

				fc_info->sortopfamily = fc_info->opfamily;
				fc_info->reverse_sort = (bool *) palloc(sizeof(bool) * fc_nkeycolumns);
				fc_info->nulls_first = (bool *) palloc(sizeof(bool) * fc_nkeycolumns);

				for (fc_i = 0; fc_i < fc_nkeycolumns; fc_i++)
				{
					int16		fc_opt = fc_indexRelation->rd_indoption[fc_i];

					fc_info->reverse_sort[fc_i] = (fc_opt & INDOPTION_DESC) != 0;
					fc_info->nulls_first[fc_i] = (fc_opt & INDOPTION_NULLS_FIRST) != 0;
				}
			}
			else if (fc_amroutine->amcanorder)
			{
				/*
				 * 否则，通过尝试将该索引的“小于”运算符映射到btree，识别相应的btree操作家庭。由于“小于”唯一定义了排序顺序的行为，因此这是一个足够的测试。
				 *
				 * XXX 这种方法相当慢，并且还需要不太理想的假设，即其他索引AM会按与btree相同的方式编号其策略。最好是有一种方法，可以明确声明其他索引类型每个操作家庭对应的btree操作家庭。但考虑到当前或可预见的amcanorder索引类型的缺乏，现在不值得再花时间去做。
				 */
				fc_info->sortopfamily = (Oid *) palloc(sizeof(Oid) * fc_nkeycolumns);
				fc_info->reverse_sort = (bool *) palloc(sizeof(bool) * fc_nkeycolumns);
				fc_info->nulls_first = (bool *) palloc(sizeof(bool) * fc_nkeycolumns);

				for (fc_i = 0; fc_i < fc_nkeycolumns; fc_i++)
				{
					int16		fc_opt = fc_indexRelation->rd_indoption[fc_i];
					Oid			fc_ltopr;
					Oid			fc_btopfamily;
					Oid			fc_btopcintype;
					int16		fc_btstrategy;

					fc_info->reverse_sort[fc_i] = (fc_opt & INDOPTION_DESC) != 0;
					fc_info->nulls_first[fc_i] = (fc_opt & INDOPTION_NULLS_FIRST) != 0;

					fc_ltopr = get_opfamily_member(fc_info->opfamily[fc_i],
												fc_info->opcintype[fc_i],
												fc_info->opcintype[fc_i],
												BTLessStrategyNumber);
					if (OidIsValid(fc_ltopr) &&
						get_ordering_op_properties(fc_ltopr,
												   &fc_btopfamily,
												   &fc_btopcintype,
												   &fc_btstrategy) &&
						fc_btopcintype == fc_info->opcintype[fc_i] &&
						fc_btstrategy == BTLessStrategyNumber)
					{
						/* 映射成功 */
						fc_info->sortopfamily[fc_i] = fc_btopfamily;
					}
					else
					{
						/* 失败...默默将索引视为无序 */
						fc_info->sortopfamily = NULL;
						fc_info->reverse_sort = NULL;
						fc_info->nulls_first = NULL;
						break;
					}
				}
			}
			else
			{
				fc_info->sortopfamily = NULL;
				fc_info->reverse_sort = NULL;
				fc_info->nulls_first = NULL;
			}

			/*
			 * 获取索引表达式和谓词（如果有）。我们必须修改从relcache中获得的副本，以具有父关系的正确varno，以便它们能正确匹配qual子句。
			 */
			fc_info->indexprs = RelationGetIndexExpressions(fc_indexRelation);
			fc_info->indpred = RelationGetIndexPredicate(fc_indexRelation);
			if (fc_info->indexprs && fc_varno != 1)
				ChangeVarNodes((Node *) fc_info->indexprs, 1, fc_varno, 0);
			if (fc_info->indpred && fc_varno != 1)
				ChangeVarNodes((Node *) fc_info->indpred, 1, fc_varno, 0);

			/* 使用完成的indexprs数据构建目标列表 */
			fc_info->indextlist = fc_build_index_tlist(fc_root, fc_info, fc_relation);

			fc_info->indrestrictinfo = NIL;	/* 将在indxpath.c中稍后设置 */
			fc_info->predOK = false;	/* 将在indxpath.c中稍后设置 */
			fc_info->unique = fc_index->indisunique;
			fc_info->immediate = fc_index->indimmediate;
			fc_info->hypothetical = false;

			/*
			 * 估算索引大小。如果这不是部分索引，我们将锁定元组数量的估算值等于父表；如果是部分索引，则必须使用与表相同的方法，除了我们可以确保索引不大于表。
			 */
			if (fc_info->indpred == NIL)
			{
				fc_info->pages = RelationGetNumberOfBlocks(fc_indexRelation);
				fc_info->tuples = fc_rel->tuples;
			}
			else
			{
				double		fc_allvisfrac; /* 虚拟占位符 */

				estimate_rel_size(fc_indexRelation, NULL,
								  &fc_info->pages, &fc_info->tuples, &fc_allvisfrac);
				if (fc_info->tuples > fc_rel->tuples)
					fc_info->tuples = fc_rel->tuples;
			}

			if (fc_info->relam == BTREE_AM_OID)
			{
				/* 对于btree，在索引打开时获取树高 */
				fc_info->tree_height = _bt_getrootheight(fc_indexRelation);
			}
			else
			{
				/* 对于其他类型的索引，目前只将其设置为“未知” */
				fc_info->tree_height = -1;
			}

			index_close(fc_indexRelation, NoLock);

			/*
			 * 我们历史上在这里使用lcons()。使用lappend()会更有道理，但这会导致规划器在两个索引似乎同样吸引的情况下改变行为。目前，继续使用lcons() --- 很少有表会有这么多索引，以至于lcons()的O(N^2)行为真的是个问题。
			 */
			fc_indexinfos = lcons(fc_info, fc_indexinfos);
		}

		list_free(fc_indexoidlist);
	}

	fc_rel->indexlist = fc_indexinfos;

	fc_rel->statlist = get_relation_statistics(fc_rel, fc_relation);

	/* 在我们获得relcache时抓取外部表信息 */
	if (fc_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
	{
		/* 检查对外部表的访问是否受到限制 */
		if (unlikely((restrict_nonsystem_relation_kind & RESTRICT_RELKIND_FOREIGN_TABLE) != 0))
		{
			/* 不得有内置的外部表 */
			Assert(RelationGetRelid(fc_relation) >= FirstNormalObjectId);

			ereport(ERROR,
					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
					 errmsg("access to non-system foreign table is restricted")));
		}

		fc_rel->serverid = GetForeignServerIdByRelId(RelationGetRelid(fc_relation));
		fc_rel->fdwroutine = GetFdwRoutineForRelation(fc_relation, true);
	}
	else
	{
		fc_rel->serverid = InvalidOid;
		fc_rel->fdwroutine = NULL;
	}

	/* 收集关于关系外键的信息（如果相关） */
	get_relation_foreign_keys(fc_root, fc_rel, fc_relation, fc_inhparent);

	/* 收集由关系的表AM实现的函数的信息。 */
	if (fc_relation->rd_tableam &&
		fc_relation->rd_tableam->scan_set_tidrange != NULL &&
		fc_relation->rd_tableam->scan_getnextslot_tidrange != NULL)
		fc_rel->amflags |= AMFLAG_HAS_TID_RANGE;

	/*
	 * 收集有关关系分区方案的信息（如果有）。只有继承父类可以被分区。
	 */
	if (fc_inhparent && fc_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
		set_relation_partition_info(fc_root, fc_rel, fc_relation);

	table_close(fc_relation, NoLock);

	/*
	 * 允许插件对我们从目录中获得的信息进行编辑。操作可能包括更改假定的关系大小、删除索引或向索引列表添加假设索引。
	 */
	if (get_relation_info_hook)
		(*get_relation_info_hook) (fc_root, fc_relationObjectId, fc_inhparent, fc_rel);
}

/*
 * get_relation_foreign_keys -
 *	  检索给定关系的外键信息。
 *
 * 为相关外键创建ForeignKeyOptInfos并将其添加到root->fkey_list。在我们拥有relcache条目打开时执行此操作。有时，如果我们等到所有RelOptInfos都构建完成，我们可能会避免创建无用的ForeignKeyOptInfos，但重新打开relcache条目的成本可能会超过任何节省。
 */
static void get_relation_foreign_keys(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						  Relation fc_relation, bool fc_inhparent)
{
	List	   *fc_rtable = fc_root->parse->rtable;
	List	   *fc_cachedfkeys;
	ListCell   *fc_lc;

	/*
	 * 如果它不是一个baserel，我们不关心它的外键。此外，如果查询仅引用单个关系，我们可以跳过查找，因为没有外键可以满足以下要求。
	 */
	if (fc_rel->reloptkind != RELOPT_BASEREL ||
		list_length(fc_rtable) < 2)
		return;

	/*
	 * 如果它是继承树的父节点，则忽略其外键。如果我们发现继承树中的所有成员都有相同的外键约束，我们可以做出有用的基于外键的推论，但检测这需要尚未编写的代码。
	 */
	if (fc_inhparent)
		return;

	/*
	 * 从relcache提取有关关系外键的数据。请注意，此列表属于relcache，并可能在缓存清除时消失，因此我们必须在此函数内不再进行任何进一步的目录访问。
	 */
	fc_cachedfkeys = RelationGetFKeyList(fc_relation);

	/*
	 * 确定此查询中感兴趣的外键，并为其创建ForeignKeyOptInfos。我们只希望外键引用当前查询的某些其他RTE。在包含自连接的查询中，引用表可能会有多个其他RTE，我们应该为每个出现创建一个ForeignKeyOptInfo。
	 *
	 * 理想情况下，我们会忽略对应于非baserel的RTE，但在这里识别这些太困难了，因此我们最终可能会创建一些无用的ForeignKeyOptInfos。如果是这样，match_foreign_keys_to_quals()将再次将它们移除。
	 */
	foreach(fc_lc, fc_cachedfkeys)
	{
		ForeignKeyCacheInfo *fc_cachedfk = (ForeignKeyCacheInfo *) lfirst(fc_lc);
		Index		fc_rti;
		ListCell   *fc_lc2;

		/* conrelid应该始终是我们考虑的表的ID */
		Assert(fc_cachedfk->conrelid == RelationGetRelid(fc_relation));

		/* 扫描查找匹配confrelid的其他RTE */
		fc_rti = 0;
		foreach(fc_lc2, fc_rtable)
		{
			RangeTblEntry *fc_rte = (RangeTblEntry *) lfirst(fc_lc2);
			ForeignKeyOptInfo *fc_info;

			fc_rti++;
			/* 如果不是正确的表，忽略 */
			if (fc_rte->rtekind != RTE_RELATION ||
				fc_rte->relid != fc_cachedfk->confrelid)
				continue;
			/* 如果是继承父类，忽略；并不真正匹配 */
			if (fc_rte->inh)
				continue;
			/* 忽略自引用的外键；我们只关心连接 */
			if (fc_rti == fc_rel->relid)
				continue;

			/* 好吧，来创建一个条目 */
			fc_info = makeNode(ForeignKeyOptInfo);
			fc_info->con_relid = fc_rel->relid;
			fc_info->ref_relid = fc_rti;
			fc_info->nkeys = fc_cachedfk->nkeys;
			memcpy(fc_info->conkey, fc_cachedfk->conkey, sizeof(fc_info->conkey));
			memcpy(fc_info->confkey, fc_cachedfk->confkey, sizeof(fc_info->confkey));
			memcpy(fc_info->conpfeqop, fc_cachedfk->conpfeqop, sizeof(fc_info->conpfeqop));
			/* 将字段归零，以便由match_foreign_keys_to_quals填充 */
			fc_info->nmatched_ec = 0;
			fc_info->nconst_ec = 0;
			fc_info->nmatched_rcols = 0;
			fc_info->nmatched_ri = 0;
			memset(fc_info->eclass, 0, sizeof(fc_info->eclass));
			memset(fc_info->fk_eclass_member, 0, sizeof(fc_info->fk_eclass_member));
			memset(fc_info->rinfos, 0, sizeof(fc_info->rinfos));

			fc_root->fkey_list = lappend(fc_root->fkey_list, fc_info);
		}
	}
}


List * infer_arbiter_indexes(PlannerInfo *fc_root)
{
	OnConflictExpr *fc_onconflict = fc_root->parse->onConflict;

	/* 迭代状态 */
	Index		fc_varno;
	RangeTblEntry *fc_rte;
	Relation	fc_relation;
	Oid			fc_indexOidFromConstraint = InvalidOid;
	List	   *fc_indexList;
	ListCell   *fc_l;

	/* 归一化推理属性和推理表达式： */
	Bitmapset  *fc_inferAttrs = NULL;
	List	   *fc_inferElems = NIL;

	/* Results */
	List	   *fc_results = NIL;

	/*
	 * 在没有推理规范或命名约束的情况下，快速返回 NIL 的 ON CONFLICT DO NOTHING。
	 * ON CONFLICT DO UPDATE 语句必须始终提供其中一种（但是解析器应该已经捕捉到这一点）。
	 */
	if (fc_onconflict->arbiterElems == NIL &&
		fc_onconflict->constraint == InvalidOid)
		return NIL;

	/*
	 * 我们无需锁定关系，因为它已经被锁定，要么是通过重写器，要么在 expand_inherited_rtentry() 中将其添加到查询的 rangetable 中。
	 */
	fc_varno = fc_root->parse->resultRelation;
	fc_rte = rt_fetch(fc_varno, fc_root->parse->rtable);

	fc_relation = table_open(fc_rte->relid, NoLock);

	/*
	 * 构建普通索引属性的归一化/BMS 表示，以及单独的表达式项列表。这简化了与目录中索引定义的匹配。
	 */
	foreach(fc_l, fc_onconflict->arbiterElems)
	{
		InferenceElem *fc_elem = (InferenceElem *) lfirst(fc_l);
		Var		   *fc_var;
		int			fc_attno;

		if (!IsA(fc_elem->expr, Var))
		{
			/* 如果不是普通 Var，暂时将其放入 inferElems 中 */
			fc_inferElems = lappend(fc_inferElems, fc_elem->expr);
			continue;
		}

		fc_var = (Var *) fc_elem->expr;
		fc_attno = fc_var->varattno;

		if (fc_attno == 0)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("whole row unique index inference specifications are not supported")));

		fc_inferAttrs = bms_add_member(fc_inferAttrs,
									fc_attno - FirstLowInvalidHeapAttributeNumber);
	}

	/*
	 * 查找命名约束的索引。这并不是立即返回的，因为需要一些额外的合理性检查。
	 */
	if (fc_onconflict->constraint != InvalidOid)
	{
		fc_indexOidFromConstraint = get_constraint_index(fc_onconflict->constraint);

		if (fc_indexOidFromConstraint == InvalidOid)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("constraint in ON CONFLICT clause has no associated index")));
	}

	/*
	 * 使用该表示法，遍历目标关系上的索引列表，尝试找到匹配项
	 */
	fc_indexList = RelationGetIndexList(fc_relation);

	foreach(fc_l, fc_indexList)
	{
		Oid			fc_indexoid = lfirst_oid(fc_l);
		Relation	fc_idxRel;
		Form_pg_index fc_idxForm;
		Bitmapset  *fc_indexedAttrs;
		List	   *fc_idxExprs;
		List	   *fc_predExprs;
		AttrNumber	fc_natt;
		ListCell   *fc_el;

		/*
		 * 从索引的关系描述符中提取信息。获取执行器最终将使用的相同锁类型。
		 *
		 * 让执行器直接抱怨 !indimmediate 情况，因为在省略推理子句时，仍然需要在那里进行强制执行。
		 */
		fc_idxRel = index_open(fc_indexoid, fc_rte->rellockmode);
		fc_idxForm = fc_idxRel->rd_index;

		if (!fc_idxForm->indisvalid)
			goto next;

		/*
		 * 请注意，我们在这里不对 indcheckxmin 进行检查（就像 e.g. get_relation_info()），以消除候选项，因为
		 * 唯一性检查只关心最近提交的元组版本。
		 */

		/*
		 * 查找“ON constraint_name”变体的匹配项，这可能不是唯一约束。这只能是约束名称。
		 */
		if (fc_indexOidFromConstraint == fc_idxForm->indexrelid)
		{
			if (!fc_idxForm->indisunique && fc_onconflict->action == ONCONFLICT_UPDATE)
				ereport(ERROR,
						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
						 errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints")));

			fc_results = lappend_oid(fc_results, fc_idxForm->indexrelid);
			list_free(fc_indexList);
			index_close(fc_idxRel, NoLock);
			table_close(fc_relation, NoLock);
			return fc_results;
		}
		else if (fc_indexOidFromConstraint != InvalidOid)
		{
			/* 对于命名约束情况，进一步的索引工作没有意义 */
			goto next;
		}

		/*
		 * 此时仅考虑传统推理（而不是命名约束），因此如果索引不是唯一的，可以立即跳过考虑。
		 */
		if (!fc_idxForm->indisunique)
			goto next;

		/* 构建普通（非表达式）索引属性的 BMS 表示 */
		fc_indexedAttrs = NULL;
		for (fc_natt = 0; fc_natt < fc_idxForm->indnkeyatts; fc_natt++)
		{
			int			fc_attno = fc_idxRel->rd_index->indkey.values[fc_natt];

			if (fc_attno != 0)
				fc_indexedAttrs = bms_add_member(fc_indexedAttrs,
											  fc_attno - FirstLowInvalidHeapAttributeNumber);
		}

		/* 非表达式属性（如果有）必须匹配 */
		if (!bms_equal(fc_indexedAttrs, fc_inferAttrs))
			goto next;

		/* 表达式属性（如果有）必须匹配 */
		fc_idxExprs = RelationGetIndexExpressions(fc_idxRel);
		if (fc_idxExprs && fc_varno != 1)
			ChangeVarNodes((Node *) fc_idxExprs, 1, fc_varno, 0);

		foreach(fc_el, fc_onconflict->arbiterElems)
		{
			InferenceElem *fc_elem = (InferenceElem *) lfirst(fc_el);

			/*
			 * 确保推理表达式元素的排序规则/opclass 方面匹配。尽管这个循环主要关注
			 * 匹配表达式，但这是一个方便的检查点，可以检查推理元素中出现的表达式和普通（非表达式）属性。
			 */
			if (!fc_infer_collation_opclass_match(fc_elem, fc_idxRel, fc_idxExprs))
				goto next;

			/*
			 * 普通 Vars 不计入表达式元素的数量，是否满足索引定义的问题已经被考虑（它们必须）。
			 */
			if (IsA(fc_elem->expr, Var))
				continue;

			/*
			 * 在推理排序规则/opclass 匹配() 实际需要执行真正工作时，尽量避免冗余检查。
			 * 否则，检查该元素表达式是否出现在目录索引定义中。
			 */
			if (fc_elem->infercollid != InvalidOid ||
				fc_elem->inferopclass != InvalidOid ||
				list_member(fc_idxExprs, fc_elem->expr))
				continue;

			goto next;
		}

		/*
		 * 现在所有推理元素都已匹配，确保推理子句中的表达式元素没有缺少任何
		 * 目录中的表达式。当唯一索引冗余重复相同属性，或者如果属性在推理子句中冗余出现多次时，会做到这一点。
		 */
		if (list_difference(fc_idxExprs, fc_inferElems) != NIL)
			goto next;

		/*
		 * 如果是部分索引，其谓词必须由 ON CONFLICT 的 WHERE 子句隐含。
		 */
		fc_predExprs = RelationGetIndexPredicate(fc_idxRel);
		if (fc_predExprs && fc_varno != 1)
			ChangeVarNodes((Node *) fc_predExprs, 1, fc_varno, 0);

		if (!predicate_implied_by(fc_predExprs, (List *) fc_onconflict->arbiterWhere, false))
			goto next;

		fc_results = lappend_oid(fc_results, fc_idxForm->indexrelid);
next:
		index_close(fc_idxRel, NoLock);
	}

	list_free(fc_indexList);
	table_close(fc_relation, NoLock);

	if (fc_results == NIL)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
				 errmsg("there is no unique or exclusion constraint matching the ON CONFLICT specification")));

	return fc_results;
}

/*
 * infer_collation_opclass_match - 确保推理元素的 opclass/排序规则匹配
 *
 * 根据来自推理规范的唯一索引推理元素，如果指定了排序规则，或者如果指定了 opclass，验证是否存在
 * 至少一个匹配的索引属性（偶尔可能有多个）。
 * 在推理规范不包括排序规则或 opclass 的常见情况下跳过该步骤（而是匹配所有内容，无论目录中索引属性的排序规则/opclass）。
 *
 * 至少在历史上，Postgres 没有提供具有替代默认的相等性概念的排序规则或 opclasses，因此这些额外的
 * 标准应该不常需要。
 *
 * 当推理元素匹配某个已标记为索引但不匹配额外的 opclass/排序规则标准的属性时，不要立即放弃。
 * 这样做的目的是为了使实现尽可能宽容，以免目录索引属性中的冗余（或者，较少有用地，在推理规范元素中）。
 * 如果在显然冗余的索引属性（在索引内部或跨索引中的冗余）之间的排序规则实际上不同，那么就不存在这种冗余。
 *
 * 请注意，如果推理元素在一次中同时指定了 opclass 和排序规则，那么这两者必须在索引
 * 目录定义中的至少一个特定属性中匹配，以便将该推理元素视为推断/满足。
 */
static bool fc_infer_collation_opclass_match(InferenceElem *fc_elem, Relation fc_idxRel,
							  List *fc_idxExprs)
{
	AttrNumber	fc_natt;
	Oid			fc_inferopfamily = InvalidOid; /* opclass opfamily 的 OID */
	Oid			fc_inferopcinputtype = InvalidOid; /* opclass 输入类型的 OID */
	int			fc_nplain = 0;		/* # 观察到的普通属性 */

	
/*
	 * 如果推理规范元素缺少排序规则/操作类，那么不需要检查是否完全匹配。
	 */
	if (fc_elem->infercollid == InvalidOid && fc_elem->inferopclass == InvalidOid)
		return true;

	/*
	 * 查找操作族和输入类型，以便匹配索引
	 */
	if (fc_elem->inferopclass)
	{
		fc_inferopfamily = get_opclass_family(fc_elem->inferopclass);
		fc_inferopcinputtype = get_opclass_input_type(fc_elem->inferopclass);
	}

	for (fc_natt = 1; fc_natt <= fc_idxRel->rd_att->natts; fc_natt++)
	{
		Oid			fc_opfamily = fc_idxRel->rd_opfamily[fc_natt - 1];
		Oid			fc_opcinputtype = fc_idxRel->rd_opcintype[fc_natt - 1];
		Oid			fc_collation = fc_idxRel->rd_indcollation[fc_natt - 1];
		int			fc_attno = fc_idxRel->rd_index->indkey.values[fc_natt - 1];

		if (fc_attno != 0)
			fc_nplain++;

		if (fc_elem->inferopclass != InvalidOid &&
			(fc_inferopfamily != fc_opfamily || fc_inferopcinputtype != fc_opcinputtype))
		{
			/* 需要匹配操作类的属性，但没有 */
			continue;
		}

		if (fc_elem->infercollid != InvalidOid &&
			fc_elem->infercollid != fc_collation)
		{
			/* 需要匹配排序规则的属性，但没有 */
			continue;
		}

		/* 如果找到一个匹配的索引属性，就足够了 -- 返回真 */
		if (IsA(fc_elem->expr, Var))
		{
			if (((Var *) fc_elem->expr)->varattno == fc_attno)
				return true;
		}
		else if (fc_attno == 0)
		{
			Node	   *fc_nattExpr = list_nth(fc_idxExprs, (fc_natt - 1) - fc_nplain);

			/*
			 * 请注意，与像 match_index_to_operand() 这样的例程不同，我们
			 * 不需要关心 RelabelType。索引定义和推理子句都不应该包含它们。
			 */
			if (equal(fc_elem->expr, fc_nattExpr))
				return true;
		}
	}

	return false;
}

/*
 * estimate_rel_size - 估计表或索引中的页面数量和元组数量
 *
 * 我们还估计在可见性映射中标记为全可见的页面的比例，以用于索引仅扫描的估计。
 *
 * 如果 attr_widths 不是 NULL，它指向关系的 attr_widths[] 缓存的零索引条目；如果我们需要计算属性宽度以进行估计，我们会填写这个。
 */
void estimate_rel_size(Relation fc_rel, int32 *fc_attr_widths,
				  BlockNumber *fc_pages, double *fc_tuples, double *fc_allvisfrac)
{
	BlockNumber fc_curpages;
	BlockNumber fc_relpages;
	double		fc_reltuples;
	BlockNumber fc_relallvisible;
	double		fc_density;

	if (RELKIND_HAS_TABLE_AM(fc_rel->rd_rel->relkind))
	{
		table_relation_estimate_size(fc_rel, fc_attr_widths, fc_pages, fc_tuples,
									 fc_allvisfrac);
	}
	else if (fc_rel->rd_rel->relkind == RELKIND_INDEX)
	{
		/*
		 * XXX: 将这个移到回调中可能更好，单独的索引类型
		 * 例如，知道它们是否有元页面。
		 */

		/* 它有存储，可以调用 smgr */
		fc_curpages = RelationGetNumberOfBlocks(fc_rel);

		/* 报告估计的页数 */
		*fc_pages = fc_curpages;
		/* 如果关系明显为空，则快速退出 */
		if (fc_curpages == 0)
		{
			*fc_tuples = 0;
			*fc_allvisfrac = 0;
			return;
		}

		/* 将 pg_class 中的值强制转换为更理想的类型 */
		fc_relpages = (BlockNumber) fc_rel->rd_rel->relpages;
		fc_reltuples = (double) fc_rel->rd_rel->reltuples;
		fc_relallvisible = (BlockNumber) fc_rel->rd_rel->relallvisible;

		/*
		 * 在估计元组数量时折扣元页面。这是一个权宜之计，因为它对于索引结构假设得过于多。目前对于 btree、hash 和 GIN 索引是可以的，但对于 GiST 索引可能有问题。
		 */
		if (fc_relpages > 0)
		{
			fc_curpages--;
			fc_relpages--;
		}

		/* 从先前的元组密度估计元组数 */
		if (fc_reltuples >= 0 && fc_relpages > 0)
			fc_density = fc_reltuples / (double) fc_relpages;
		else
		{
			/*
			 * 如果我们没有数据，因为关系从未被清理过，
			 * 则根据属性数据类型估计元组宽度。我们在这里假设页面是完全满的，这对于表是可以的
			 * （因为它们可能尚未被 VACUUM）但是对于索引来说
			 * 可能是高估。幸运的是
			 * get_relation_info() 可以将高估限制在父表的大小。
			 *
			 * 注意：这段代码故意不考虑对齐问题，
			 * 因为（a）考虑到估计是多么粗糙，这将是画蛇添足，
			 * （b）这会在默认计划中产生平台依赖性，这对回归测试来说是个麻烦。
			 *
			 * XXX: 这个逻辑是否应该更具体于索引？
			 */
			int32		fc_tuple_width;

			fc_tuple_width = get_rel_data_width(fc_rel, fc_attr_widths);
			fc_tuple_width += MAXALIGN(SizeofHeapTupleHeader);
			fc_tuple_width += sizeof(ItemIdData);
			/* 注意：这里的整数除法是故意的 */
			fc_density = (BLCKSZ - SizeOfPageHeaderData) / fc_tuple_width;
		}
		*fc_tuples = rint(fc_density * (double) fc_curpages);

		/*
		 * 我们照常使用 relallvisible，而不是像对页面和元组计数那样进行缩放，
		 * 理论上来说，自上次 VACUUM 以来添加的任何页面最有可能未标记为全可见。但是
		 * costsize.c 想要将其转换为一个比例。
		 */
		if (fc_relallvisible == 0 || fc_curpages <= 0)
			*fc_allvisfrac = 0;
		else if ((double) fc_relallvisible >= fc_curpages)
			*fc_allvisfrac = 1;
		else
			*fc_allvisfrac = (double) fc_relallvisible / fc_curpages;
	}
	else
	{
		/*
		 * 只需使用 pg_class 中的内容。这涵盖了外部表、
		 * 序列，以及没有存储的 relkind（不应该到这里？）；
		 * 请参阅 AddNewRelationTuple() 中的初始化。请注意，如果 reltuples 为 -1，FDW 必须能够处理！
		 */
		*fc_pages = fc_rel->rd_rel->relpages;
		*fc_tuples = fc_rel->rd_rel->reltuples;
		*fc_allvisfrac = 0;
	}
}


/*
 * get_rel_data_width
 *
 * 估计关系元组的数据部分的平均宽度。
 *
 * 如果 attr_widths 不是 NULL，它指向关系的 attr_widths[] 缓存的零索引条目；根据需要使用和更新该缓存。
 *
 * 目前我们忽略已删除的列。理想情况下，这些应该包含在结果中，但我们没有任何方法获取相关信息；而且由于它们可能大多数为 NULL，因此将它们视为零宽度并不一定是错误的做法。
 */
int32 get_rel_data_width(Relation fc_rel, int32 *fc_attr_widths)
{
	int32		fc_tuple_width = 0;
	int			fc_i;

	for (fc_i = 1; fc_i <= RelationGetNumberOfAttributes(fc_rel); fc_i++)
	{
		Form_pg_attribute fc_att = TupleDescAttr(fc_rel->rd_att, fc_i - 1);
		int32		fc_item_width;

		if (fc_att->attisdropped)
			continue;

		/* 如果有，使用先前缓存的数据 */
		if (fc_attr_widths != NULL && fc_attr_widths[fc_i] > 0)
		{
			fc_tuple_width += fc_attr_widths[fc_i];
			continue;
		}

		/* 这应该与 costsize.c 中的 set_rel_width() 匹配 */
		fc_item_width = get_attavgwidth(RelationGetRelid(fc_rel), fc_i);
		if (fc_item_width <= 0)
		{
			fc_item_width = get_typavgwidth(fc_att->atttypid, fc_att->atttypmod);
			Assert(fc_item_width > 0);
		}
		if (fc_attr_widths != NULL)
			fc_attr_widths[fc_i] = fc_item_width;
		fc_tuple_width += fc_item_width;
	}

	return fc_tuple_width;
}

/*
 * get_relation_data_width
 *
 * get_rel_data_width 的外部 API：行为相同，只是必须打开 relcache 条目。
 */
int32 get_relation_data_width(Oid fc_relid, int32 *fc_attr_widths)
{
	int32		fc_result;
	Relation	fc_relation;

	/* 同上，假定关系已经锁定 */
	fc_relation = table_open(fc_relid, NoLock);

	fc_result = get_rel_data_width(fc_relation, fc_attr_widths);

	table_close(fc_relation, NoLock);

	return fc_result;
}


/*
 * get_relation_constraints
 *
 * 检索给定关系的适用约束表达式。
 *
 * 返回一个约束表达式的列表（可能为空）。每一个都已经标准化，变量的 varno 被修改为 rel->relid 指示的值。这使得这些表达式可以与来自 WHERE 的表达式轻松比较。
 *
 * 如果 include_noinherit 为 true，允许包括标记为 NO INHERIT 的约束。
 *
 * 如果 include_notnull 为 true，为每个标记为 attnotnull 的列生成并添加 "col IS NOT NULL" 表达式到结果中。
 *
 * 如果 include_partition 为 true，并且关系是一个分区，还包括分区约束。
 *
 * 注意：目前在每次规划器运行中每个关系最多只调用一次，而且在许多情况下根本不会被调用，所以似乎没有必要在 RelOptInfo 中缓存数据。
 */
static List * get_relation_constraints(PlannerInfo *fc_root,
						 Oid fc_relationObjectId, RelOptInfo *fc_rel,
						 bool fc_include_noinherit,
						 bool fc_include_notnull,
						 bool fc_include_partition)
{
	List	   *fc_result = NIL;
	Index		fc_varno = fc_rel->relid;
	Relation	fc_relation;
	TupleConstr *fc_constr;

	/*
	 * 我们假定关系已经安全锁定。
	 */
	fc_relation = table_open(fc_relationObjectId, NoLock);

	fc_constr = fc_relation->rd_att->constr;
	if (fc_constr != NULL)
	{
		int			fc_num_check = fc_constr->num_check;
		int			fc_i;

		for (fc_i = 0; fc_i < fc_num_check; fc_i++)
		{
			Node	   *fc_cexpr;

			/*
			 * 如果此约束尚未完全验证，则我们必须在此处忽略它。
			 * 如果是 NO INHERIT 且我们未被告知这是安全的，也要忽略。
			 */
			if (!fc_constr->check[fc_i].ccvalid)
				continue;
			if (fc_constr->check[fc_i].ccnoinherit && !fc_include_noinherit)
				continue;

			fc_cexpr = stringToNode(fc_constr->check[fc_i].ccbin);

			/*
			 * 运行每个表达式以进行常量简化和
			 * 标准化。这不仅仅是一种优化，而是必要的，因为我们将把它与
			 * 类似处理的 qual 子句进行比较，并且在没有这个的情况下可能无法检测有效的匹配。
			 * 这必须与 preprocess_expression() 中对 qual 子句进行的处理相匹配！ 
			 * （然而，由于我们不允许在检查约束中使用任何子查询，因此可以跳过涉及子查询的内容。）
			 */
			fc_cexpr = eval_const_expressions(fc_root, fc_cexpr);

			fc_cexpr = (Node *) canonicalize_qual((Expr *) fc_cexpr, true);

			/* 修正变量以具有期望的 varno */
			if (fc_varno != 1)
				ChangeVarNodes(fc_cexpr, 1, fc_varno, 0);

			
/*
			 * 最后，将其转换为隐式-AND格式（即，List）并
			 * 将结果项附加到我们的输出列表中。
			 */
			fc_result = list_concat(fc_result,
								 make_ands_implicit((Expr *) fc_cexpr));
		}

		/* 如果需要，以表达式形式添加NOT NULL约束 */
		if (fc_include_notnull && fc_constr->has_not_null)
		{
			int			fc_natts = fc_relation->rd_att->natts;

			for (fc_i = 1; fc_i <= fc_natts; fc_i++)
			{
				Form_pg_attribute fc_att = TupleDescAttr(fc_relation->rd_att, fc_i - 1);

				if (fc_att->attnotnull && !fc_att->attisdropped)
				{
					NullTest   *fc_ntest = makeNode(NullTest);

					fc_ntest->arg = (Expr *) makeVar(fc_varno,
												  fc_i,
												  fc_att->atttypid,
												  fc_att->atttypmod,
												  fc_att->attcollation,
												  0);
					fc_ntest->nulltesttype = IS_NOT_NULL;

					/*
					 * argisrow=false即使对于复合列也是正确的，
					 * 因为attnotnull在这种情况下并不代表SQL规范中的IS NOT
					 * NULL测试，仅为IS DISTINCT FROM NULL。
					 */
					fc_ntest->argisrow = false;
					fc_ntest->location = -1;
					fc_result = lappend(fc_result, fc_ntest);
				}
			}
		}
	}

	/*
	 * 如果需要，添加分区约束。
	 */
	if (fc_include_partition && fc_relation->rd_rel->relispartition)
	{
		/* 确保rel->partition_qual被设置 */
		set_baserel_partition_constraint(fc_relation, fc_rel);
		fc_result = list_concat(fc_result, fc_rel->partition_qual);
	}

	table_close(fc_relation, NoLock);

	return fc_result;
}

/*
 * 尝试加载统计对象的数据。
 *
 * 我们不知道由statOid和inh值指定的数据是否存在。
 * 结果存储在stainfos列表中。
 */
static void get_relation_statistics_worker(List **fc_stainfos, RelOptInfo *fc_rel,
							   Oid fc_statOid, bool fc_inh,
							   Bitmapset *fc_keys, List *fc_exprs)
{
	Form_pg_statistic_ext_data fc_dataForm;
	HeapTuple	fc_dtup;

	fc_dtup = SearchSysCache2(STATEXTDATASTXOID,
						   ObjectIdGetDatum(fc_statOid), BoolGetDatum(fc_inh));
	if (!HeapTupleIsValid(fc_dtup))
		return;

	fc_dataForm = (Form_pg_statistic_ext_data) GETSTRUCT(fc_dtup);

	/* 为每种生成的统计信息添加一个StatisticExtInfo */
	if (statext_is_kind_built(fc_dtup, STATS_EXT_NDISTINCT))
	{
		StatisticExtInfo *fc_info = makeNode(StatisticExtInfo);

		fc_info->statOid = fc_statOid;
		fc_info->inherit = fc_dataForm->stxdinherit;
		fc_info->rel = fc_rel;
		fc_info->kind = STATS_EXT_NDISTINCT;
		fc_info->keys = bms_copy(fc_keys);
		fc_info->exprs = fc_exprs;

		*fc_stainfos = lappend(*fc_stainfos, fc_info);
	}

	if (statext_is_kind_built(fc_dtup, STATS_EXT_DEPENDENCIES))
	{
		StatisticExtInfo *fc_info = makeNode(StatisticExtInfo);

		fc_info->statOid = fc_statOid;
		fc_info->inherit = fc_dataForm->stxdinherit;
		fc_info->rel = fc_rel;
		fc_info->kind = STATS_EXT_DEPENDENCIES;
		fc_info->keys = bms_copy(fc_keys);
		fc_info->exprs = fc_exprs;

		*fc_stainfos = lappend(*fc_stainfos, fc_info);
	}

	if (statext_is_kind_built(fc_dtup, STATS_EXT_MCV))
	{
		StatisticExtInfo *fc_info = makeNode(StatisticExtInfo);

		fc_info->statOid = fc_statOid;
		fc_info->inherit = fc_dataForm->stxdinherit;
		fc_info->rel = fc_rel;
		fc_info->kind = STATS_EXT_MCV;
		fc_info->keys = bms_copy(fc_keys);
		fc_info->exprs = fc_exprs;

		*fc_stainfos = lappend(*fc_stainfos, fc_info);
	}

	if (statext_is_kind_built(fc_dtup, STATS_EXT_EXPRESSIONS))
	{
		StatisticExtInfo *fc_info = makeNode(StatisticExtInfo);

		fc_info->statOid = fc_statOid;
		fc_info->inherit = fc_dataForm->stxdinherit;
		fc_info->rel = fc_rel;
		fc_info->kind = STATS_EXT_EXPRESSIONS;
		fc_info->keys = bms_copy(fc_keys);
		fc_info->exprs = fc_exprs;

		*fc_stainfos = lappend(*fc_stainfos, fc_info);
	}

	ReleaseSysCache(fc_dtup);
}

/*
 * get_relation_statistics
 *		检索在表上定义的扩展统计信息。
 *
 * 返回一个List（可能为空）的StatisticExtInfo对象，
 * 描述统计信息。注意，这并不加载实际的统计数据，
 * 只加载识别元数据。只有实际构建的统计信息才会被考虑。
 */
static List * get_relation_statistics(RelOptInfo *fc_rel, Relation fc_relation)
{
	Index		fc_varno = fc_rel->relid;
	List	   *fc_statoidlist;
	List	   *fc_stainfos = NIL;
	ListCell   *fc_l;

	fc_statoidlist = RelationGetStatExtList(fc_relation);

	foreach(fc_l, fc_statoidlist)
	{
		Oid			fc_statOid = lfirst_oid(fc_l);
		Form_pg_statistic_ext fc_staForm;
		HeapTuple	fc_htup;
		Bitmapset  *fc_keys = NULL;
		List	   *fc_exprs = NIL;
		int			fc_i;

		fc_htup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(fc_statOid));
		if (!HeapTupleIsValid(fc_htup))
			elog(ERROR, "cache lookup failed for statistics object %u", fc_statOid);
		fc_staForm = (Form_pg_statistic_ext) GETSTRUCT(fc_htup);

		/*
		 * 首先，构建被覆盖的列的数组。如果对象内没有实际构建的统计信息，这最终是浪费的，但
		 * 看起来没有必要担心这种情况。
		 */
		for (fc_i = 0; fc_i < fc_staForm->stxkeys.dim1; fc_i++)
			fc_keys = bms_add_member(fc_keys, fc_staForm->stxkeys.values[fc_i]);

		/*
		 * 预处理表达式（如果有）。我们读取表达式，通过eval_const_expressions运行它们，
		 * 并修复varnos。
		 *
		 * XXX 我们还不知道这个统计对象是否有任何数据，
		 * 无论是stxdinherit值。但合理的假设是至少有其中一个，可能两个都有。因此，
		 * 在这里处理关键字和表达式是更好的选择。
		 */
		{
			bool		fc_isnull;
			Datum		fc_datum;

			/* 解码表达式（如果有） */
			fc_datum = SysCacheGetAttr(STATEXTOID, fc_htup,
									Anum_pg_statistic_ext_stxexprs, &fc_isnull);

			if (!fc_isnull)
			{
				char	   *fc_exprsString;

				fc_exprsString = TextDatumGetCString(fc_datum);
				fc_exprs = (List *) stringToNode(fc_exprsString);
				pfree(fc_exprsString);

				/*
				 * 通过 eval_const_expressions 运行表达式。这不仅是一个优化，而且是必要的，因为查询优化器将它们与同样处理过的条件子句进行比较，如果没有这个，可能无法检测到有效匹配。
				 * 然而，我们必须不使用 canonicalize_qual，因为这些不是条件表达式。
				 */
				fc_exprs = (List *) eval_const_expressions(NULL, (Node *) fc_exprs);

				/* 也可以修复 opfuncids */
				fix_opfuncids((Node *) fc_exprs);

				/*
				 * 修改从 relcache 获取的副本，以具有父关系的正确 varno，以便它们能够正确匹配条件子句。
				 */
				if (fc_varno != 1)
					ChangeVarNodes((Node *) fc_exprs, 1, fc_varno, 0);
			}
		}

		/* 提取 stxdinherit 标志的可能值的统计信息 */

		get_relation_statistics_worker(&fc_stainfos, fc_rel, fc_statOid, true, fc_keys, fc_exprs);

		get_relation_statistics_worker(&fc_stainfos, fc_rel, fc_statOid, false, fc_keys, fc_exprs);

		ReleaseSysCache(fc_htup);
		bms_free(fc_keys);
	}

	list_free(fc_statoidlist);

	return fc_stainfos;
}

/*
 * relation_excluded_by_constraints
 *
 * 检测是否需要扫描该关系，因为它具有自自洽的限制或与该关系适用的约束不一致的限制。
 *
 * 注意：这仅检查 rel->relid、rel->reloptkind 和 rel->baserestrictinfo；因此可以在填写 RelOptInfo 的其他字段之前调用它。
 */
bool relation_excluded_by_constraints(PlannerInfo *fc_root,
								 RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	bool		fc_include_noinherit;
	bool		fc_include_notnull;
	bool		fc_include_partition = false;
	List	   *fc_safe_restrictions;
	List	   *fc_constraint_pred;
	List	   *fc_safe_constraints;
	ListCell   *fc_lc;

	/* 截至目前，约束排除仅适用于简单关系。 */
	Assert(IS_SIMPLE_REL(fc_rel));

	/*
	 * 如果没有基本限制子句，我们就没有希望证明以下任何内容，因此迅速退出。
	 */
	if (fc_rel->baserestrictinfo == NIL)
		return false;

	/*
	 * 不管 constraint_exclusion 的设置如何，检测常量-FALSE或-NULL 限制子句。因为常量折叠会将“任何 AND FALSE”减少到仅“FALSE”，任何这样的情况应该导致恰好一个 baserestrictinfo 条目。这个情况并不常发生，但考虑到其成本相对较低，做这个仍然是值得的。（如果没有这个，我们会错过一些 9.5 及更早版本通过更绕的方式找到的优化。）
	 */
	if (list_length(fc_rel->baserestrictinfo) == 1)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) linitial(fc_rel->baserestrictinfo);
		Expr	   *fc_clause = fc_rinfo->clause;

		if (fc_clause && IsA(fc_clause, Const) &&
			(((Const *) fc_clause)->constisnull ||
			 !DatumGetBool(((Const *) fc_clause)->constvalue)))
			return true;
	}

	/*
	 * 根据 constraint_exclusion 跳过进一步的测试。
	 */
	switch (constraint_exclusion)
	{
		case CONSTRAINT_EXCLUSION_OFF:
			/* 在“关闭”模式下，绝不要进行进一步测试 */
			return false;

		case CONSTRAINT_EXCLUSION_PARTITION:

			/*
			 * 当 constraint_exclusion 设置为“分区”时，我们仅处理附加关系成员。分区修剪已被应用，因此这里不需要考虑关系的分区约束。
			 */
			if (fc_rel->reloptkind == RELOPT_OTHER_MEMBER_REL)
				break;			/* 附加关系成员，因此处理它 */
			return false;

		case CONSTRAINT_EXCLUSION_ON:

			/*
			 * 在“开启”模式下，始终应用约束排除。如果我们考虑的基关系是一个分区（即，它是直接命名而不是从父表扩展的），那么它的分区约束尚未被考虑，因此在这里包括它们进行处理。
			 */
			if (fc_rel->reloptkind == RELOPT_BASEREL)
				fc_include_partition = true;
			break;				/* 始终尝试排除 */
	}

	/*
	 * 检查自相矛盾的限制子句。我们不敢对非不可变函数做出推论，但任何自相矛盾的不可变子句都允许我们得出扫描是不必要的结论。
	 *
	 * 注意：剥离 RestrictInfo，因为 predicate_refuted_by() 不期望在其谓词参数中看到任何内容。
	 */
	fc_safe_restrictions = NIL;
	foreach(fc_lc, fc_rel->baserestrictinfo)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (!contain_mutable_functions((Node *) fc_rinfo->clause))
			fc_safe_restrictions = lappend(fc_safe_restrictions, fc_rinfo->clause);
	}

	/*
	 * 我们可以在这里使用弱反驳，因为我们正在比较限制子句与限制子句。
	 */
	if (predicate_refuted_by(fc_safe_restrictions, fc_safe_restrictions, true))
		return true;

	/*
	 * 只有普通关系具有约束，因此对于其他 rtekinds 停止。
	 */
	if (fc_rte->rtekind != RTE_RELATION)
		return false;

	/*
	 * 如果我们仅扫描此表，则可以使用 NO INHERIT 约束，但如果我们也扫描它的子表则不可以。（注意，分区表不应有 NO INHERIT 约束；但我们在这里并不需要假设这一点。）
	 */
	fc_include_noinherit = !fc_rte->inh;

	/*
	 * 当前，attnotnull 约束必须作为 NO INHERIT 处理，除非这是一个分区表。在未来，我们可能会更准确地跟踪它们的继承状态，从而允许对此进行细化。
	 */
	fc_include_notnull = (!fc_rte->inh || fc_rte->relkind == RELKIND_PARTITIONED_TABLE);

	/*
	 * 获取适当的约束表达式集合。
	 */
	fc_constraint_pred = get_relation_constraints(fc_root, fc_rte->relid, fc_rel,
											   fc_include_noinherit,
											   fc_include_notnull,
											   fc_include_partition);

	/*
	 * 我们目前不强制 CHECK 约束仅包含不可变函数，因此这里有必要进行检查。我们不敢从非不可变函数的计划时评估中得出结论。由于它们是 AND 连接的，我们可以忽略列表中的任何可变约束，并对其余部分进行推理。
	 */
	fc_safe_constraints = NIL;
	foreach(fc_lc, fc_constraint_pred)
	{
		Node	   *fc_pred = (Node *) lfirst(fc_lc);

		if (!contain_mutable_functions(fc_pred))
			fc_safe_constraints = lappend(fc_safe_constraints, fc_pred);
	}

	/*
	 * 约束有效地被 AND 结合在一起，因此我们可以尝试一次否决整个集合。这可能允许我们进行在逐个进行时会失败的证明。
	 *
	 * 注意：我们使用 rel->baserestrictinfo，而不是 safe_restrictions，尽管这看起来是一个明显的优化。一些子句可能是包含可变和非可变子句的 OR 子句，使用非可变部分进行推理是可以的。
	 *
	 * 我们需要强反驳，因为我们必须证明约束会导致错误，而不仅仅是 NULL。
	 */
	if (predicate_refuted_by(fc_safe_constraints, fc_rel->baserestrictinfo, false))
		return true;

	return false;
}



/*
 * build_physical_tlist
 *
 * 构建一个目标列表，精确包含关系的用户属性，按顺序排列。执行器可以对这样的目标列表进行特殊处理，以避免在运行时进行投影步骤，因此我们优先对扫描节点使用这样的目标列表。
 *
 * 例外情况：如果有任何被丢弃或缺失的列，我们将放弃并返回 NIL。理想情况下，我们也希望处理这些情况。然而，这会导致 ExecTypeFromTL 遇到问题，因为它可能被要求为包含不存在类型的变量的目标列表构建 tupdesc。理论上，我们可以从关系的 pg_attribute 条目挖掘出所需的信息，但这些数据并不容易获得给 ExecTypeFromTL。目前，当存在被丢弃的列时，我们不应用物理目标列表优化。
 *
 * 我们还支持为子查询、函数、值列表、表表达式和 CTE 构建“物理”目标列表，因为相同的优化可以发生在 SubqueryScan、FunctionScan、ValuesScan、CteScan、TableFunc、NamedTuplestoreScan 和 WorkTableScan 节点中。
 */
List * build_physical_tlist(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	List	   *fc_tlist = NIL;
	Index		fc_varno = fc_rel->relid;
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_varno, fc_root);
	Relation	fc_relation;
	Query	   *fc_subquery;
	Var		   *fc_var;
	ListCell   *fc_l;
	int			fc_attrno,
				fc_numattrs;
	List	   *fc_colvars;

	switch (fc_rte->rtekind)
	{
		case RTE_RELATION:
			/* 假设我们已经拥有足够的锁 */
			fc_relation = table_open(fc_rte->relid, NoLock);

			fc_numattrs = RelationGetNumberOfAttributes(fc_relation);
			for (fc_attrno = 1; fc_attrno <= fc_numattrs; fc_attrno++)
			{
				Form_pg_attribute fc_att_tup = TupleDescAttr(fc_relation->rd_att,
														  fc_attrno - 1);

				if (fc_att_tup->attisdropped || fc_att_tup->atthasmissing)
				{
					/* 找到被丢弃或缺失的列，因此放弃 */
					fc_tlist = NIL;
					break;
				}

				fc_var = makeVar(fc_varno,
							  fc_attrno,
							  fc_att_tup->atttypid,
							  fc_att_tup->atttypmod,
							  fc_att_tup->attcollation,
							  0);

				fc_tlist = lappend(fc_tlist,
								makeTargetEntry((Expr *) fc_var,
												fc_attrno,
												NULL,
												false));
			}

			table_close(fc_relation, NoLock);
			break;

		case RTE_SUBQUERY:
			fc_subquery = fc_rte->subquery;
			foreach(fc_l, fc_subquery->targetList)
			{
				TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_l);

				/*
				 * 子查询的 resjunk 列可以在物理目标列表中反映为 resjunk；我们不需要放弃。
				 */
				fc_var = makeVarFromTargetEntry(fc_varno, fc_tle);

				fc_tlist = lappend(fc_tlist,
								makeTargetEntry((Expr *) fc_var,
												fc_tle->resno,
												NULL,
												fc_tle->resjunk));
			}
			break;

		case RTE_FUNCTION:
		case RTE_TABLEFUNC:
		case RTE_VALUES:
		case RTE_CTE:
		case RTE_NAMEDTUPLESTORE:
		case RTE_RESULT:
			/* 并非所有这些列都可以有被丢弃的列，但代码仍然共享 */
			expandRTE(fc_rte, fc_varno, 0, -1, true /* 包括被丢弃的列 */ ,
					  NULL, &fc_colvars);
			foreach(fc_l, fc_colvars)
			{
				fc_var = (Var *) lfirst(fc_l);

				/*
				 * expandRTE 的输出中的非 Var 代表被丢弃的列；必须放弃。
				 */
				if (!IsA(fc_var, Var))
				{
					fc_tlist = NIL;
					break;
				}

				fc_tlist = lappend(fc_tlist,
								makeTargetEntry((Expr *) fc_var,
												fc_var->varattno,
												NULL,
												false));
			}
			break;

		default:
			/* 调用者错误 */
			elog(ERROR, "unsupported RTE kind %d in build_physical_tlist",
				 (int) fc_rte->rtekind);
			break;
	}

	return fc_tlist;
}

/*
 * build_index_tlist
 *
 * 构建一个目标列表，表示指定索引的列。
 * 每一列由相应基表列的 Var 表示，或者根据需要表示为基表 Var 中的表达式。
 *
 * 索引中从没有删除的列，因此与 build_physical_tlist 不同，我们不需要失败情况。
 */
static List * fc_build_index_tlist(PlannerInfo *fc_root, IndexOptInfo *fc_index,
				  Relation fc_heapRelation)
{
	List	   *fc_tlist = NIL;
	Index		fc_varno = fc_index->rel->relid;
	ListCell   *fc_indexpr_item;
	int			fc_i;

	fc_indexpr_item = list_head(fc_index->indexprs);
	for (fc_i = 0; fc_i < fc_index->ncolumns; fc_i++)
	{
		int			fc_indexkey = fc_index->indexkeys[fc_i];
		Expr	   *fc_indexvar;

		if (fc_indexkey != 0)
		{
			/* 简单列 */
			const FormData_pg_attribute *fc_att_tup;

			if (fc_indexkey < 0)
				fc_att_tup = SystemAttributeDefinition(fc_indexkey);
			else
				fc_att_tup = TupleDescAttr(fc_heapRelation->rd_att, fc_indexkey - 1);

			fc_indexvar = (Expr *) makeVar(fc_varno,
										fc_indexkey,
										fc_att_tup->atttypid,
										fc_att_tup->atttypmod,
										fc_att_tup->attcollation,
										0);
		}
		else
		{
			/* 表达式列 */
			if (fc_indexpr_item == NULL)
				elog(ERROR, "wrong number of index expressions");
			fc_indexvar = (Expr *) lfirst(fc_indexpr_item);
			fc_indexpr_item = lnext(fc_index->indexprs, fc_indexpr_item);
		}

		fc_tlist = lappend(fc_tlist,
						makeTargetEntry(fc_indexvar,
										fc_i + 1,
										NULL,
										false));
	}
	if (fc_indexpr_item != NULL)
		elog(ERROR, "wrong number of index expressions");

	return fc_tlist;
}

/*
 * restriction_selectivity
 *
 * 返回指定限制操作符子句的选择性。
 * 这段代码通过调用函数管理器执行存储在操作符关系中的注册过程。
 *
 * 请参阅 clause_selectivity() 以获取附加参数的含义。
 */
Selectivity restriction_selectivity(PlannerInfo *fc_root,
						Oid fc_operatorid,
						List *fc_args,
						Oid fc_inputcollid,
						int fc_varRelid)
{
	RegProcedure fc_oprrest = get_oprrest(fc_operatorid);
	float8		fc_result;

	/*
	 * 如果由于某种原因缺少 oprrest 过程，使用选择性 0.5。
	 */
	if (!fc_oprrest)
		return (Selectivity) 0.5;

	fc_result = DatumGetFloat8(OidFunctionCall4Coll(fc_oprrest,
												 fc_inputcollid,
												 PointerGetDatum(fc_root),
												 ObjectIdGetDatum(fc_operatorid),
												 PointerGetDatum(fc_args),
												 Int32GetDatum(fc_varRelid)));

	if (fc_result < 0.0 || fc_result > 1.0)
		elog(ERROR, "invalid restriction selectivity: %f", fc_result);

	return (Selectivity) fc_result;
}

/*
 * join_selectivity
 *
 * 返回指定连接操作符子句的选择性。
 * 这段代码通过调用函数管理器执行存储在操作符关系中的注册过程。
 *
 * 请参阅 clause_selectivity() 以获取附加参数的含义。
 */
Selectivity join_selectivity(PlannerInfo *fc_root,
				 Oid fc_operatorid,
				 List *fc_args,
				 Oid fc_inputcollid,
				 JoinType fc_jointype,
				 SpecialJoinInfo *fc_sjinfo)
{
	RegProcedure fc_oprjoin = get_oprjoin(fc_operatorid);
	float8		fc_result;

	/*
	 * 如果由于某种原因缺少 oprjoin 过程，使用选择性 0.5。
	 */
	if (!fc_oprjoin)
		return (Selectivity) 0.5;

	fc_result = DatumGetFloat8(OidFunctionCall5Coll(fc_oprjoin,
												 fc_inputcollid,
												 PointerGetDatum(fc_root),
												 ObjectIdGetDatum(fc_operatorid),
												 PointerGetDatum(fc_args),
												 Int16GetDatum(fc_jointype),
												 PointerGetDatum(fc_sjinfo)));

	if (fc_result < 0.0 || fc_result > 1.0)
		elog(ERROR, "invalid join selectivity: %f", fc_result);

	return (Selectivity) fc_result;
}

/*
 * function_selectivity
 *
 * 返回指定布尔函数子句的选择性。
 * 这段代码通过调用函数管理器执行存储在 pg_proc 关系中的注册过程。
 *
 * 请参阅 clause_selectivity() 以获取附加参数的含义。
 */
Selectivity function_selectivity(PlannerInfo *fc_root,
					 Oid fc_funcid,
					 List *fc_args,
					 Oid fc_inputcollid,
					 bool fc_is_join,
					 int fc_varRelid,
					 JoinType fc_jointype,
					 SpecialJoinInfo *fc_sjinfo)
{
	RegProcedure fc_prosupport = get_func_support(fc_funcid);
	SupportRequestSelectivity fc_req;
	SupportRequestSelectivity *fc_sresult;

	/*
	 * 如果没有提供支持函数，使用我们历史上的默认估计值 0.3333333。
	 * 这似乎是一个相当不原则的选择，但 Postgres 自 1992 年以来一直在使用该估计值用于函数调用。
	 * 这种行为的悠久历史表明，我们不应急于使用其他值。
	 */
	if (!fc_prosupport)
		return (Selectivity) 0.3333333;

	fc_req.type = T_SupportRequestSelectivity;
	fc_req.root = fc_root;
	fc_req.funcid = fc_funcid;
	fc_req.args = fc_args;
	fc_req.inputcollid = fc_inputcollid;
	fc_req.is_join = fc_is_join;
	fc_req.varRelid = fc_varRelid;
	fc_req.jointype = fc_jointype;
	fc_req.sjinfo = fc_sjinfo;
	fc_req.selectivity = -1;		/* 捕捉未设置值的失败 */

	fc_sresult = (SupportRequestSelectivity *)
		DatumGetPointer(OidFunctionCall1(fc_prosupport,
										 PointerGetDatum(&fc_req)));

	/* 如果支持函数失败，使用默认值 */
	if (fc_sresult != &fc_req)
		return (Selectivity) 0.3333333;

	if (fc_req.selectivity < 0.0 || fc_req.selectivity > 1.0)
		elog(ERROR, "invalid function selectivity: %f", fc_req.selectivity);

	return (Selectivity) fc_req.selectivity;
}

/*
 * add_function_cost
 *
 * 获取函数的执行成本估计，并将其 *添加* 到 *cost* 的内容中。
 * 该估计可能包括一次性和每个元组组件，因为 QualCost 也是如此。
 *
 * funcid 必须始终提供。如果它作为特定解析树节点（FuncExpr，OpExpr，WindowFunc 等）的实现被调用，请将其作为 "node" 传递，否则传递 NULL。
 *
 * 在某些用法中，root 也可能为 NULL。
 */
void add_function_cost(PlannerInfo *fc_root, Oid fc_funcid, Node *fc_node,
				  QualCost *fc_cost)
{
	HeapTuple	fc_proctup;
	Form_pg_proc fc_procform;

	fc_proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fc_funcid));
	if (!HeapTupleIsValid(fc_proctup))
		elog(ERROR, "cache lookup failed for function %u", fc_funcid);
	fc_procform = (Form_pg_proc) GETSTRUCT(fc_proctup);

	if (OidIsValid(fc_procform->prosupport))
	{
		SupportRequestCost fc_req;
		SupportRequestCost *fc_sresult;

		fc_req.type = T_SupportRequestCost;
		fc_req.root = fc_root;
		fc_req.funcid = fc_funcid;
		fc_req.node = fc_node;

		/* 初始化成本字段，以便支持函数不必 */
		fc_req.startup = 0;
		fc_req.per_tuple = 0;

		fc_sresult = (SupportRequestCost *)
			DatumGetPointer(OidFunctionCall1(fc_procform->prosupport,
											 PointerGetDatum(&fc_req)));

		if (fc_sresult == &fc_req)
		{
			/* 成功，因此将支持函数的估计累积到 *cost* 中 */
			fc_cost->startup += fc_req.startup;
			fc_cost->per_tuple += fc_req.per_tuple;
			ReleaseSysCache(fc_proctup);
			return;
		}
	}

	/* 没有支持函数，或它失败了，因此依赖于 procost */
	fc_cost->per_tuple += fc_procform->procost * cpu_operator_cost;

	ReleaseSysCache(fc_proctup);
}

/*
 * get_function_rows
 *
 * 获取 set-returning 函数返回的估计行数。
 *
 * funcid 必须始终提供。在当前用法中，调用节点将始终被提供，并且将是 FuncExpr 或 OpExpr。
 * 但不应在为 NULL 时失败是个好主意。
 *
 * 在某些用法中，root 也可能为 NULL。
 *
 * 注意：如果有支持函数，这将返回未经筛选的结果。
 * 通常最好对结果应用 clamp_row_est()，但我们将其留给调用者执行。
 */
double get_function_rows(PlannerInfo *fc_root, Oid fc_funcid, Node *fc_node)
{
	HeapTuple	fc_proctup;
	Form_pg_proc fc_procform;
	double		fc_result;

	fc_proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fc_funcid));
	if (!HeapTupleIsValid(fc_proctup))
		elog(ERROR, "cache lookup failed for function %u", fc_funcid);
	fc_procform = (Form_pg_proc) GETSTRUCT(fc_proctup);

	Assert(fc_procform->proretset);	/*
		 * 我们使用 CaseTestExpr 作为强制转换树的基础，
		 * 因为插入源值是非常便宜的。
		 */

	if (OidIsValid(fc_procform->prosupport))
	{
		SupportRequestRows fc_req;
		SupportRequestRows *fc_sresult;

		fc_req.type = T_SupportRequestRows;
		fc_req.root = fc_root;
		fc_req.funcid = fc_funcid;
		fc_req.node = fc_node;

		fc_req.rows = 0;			/* 只是为了 sanity */

		fc_sresult = (SupportRequestRows *)
			DatumGetPointer(OidFunctionCall1(fc_procform->prosupport,
											 PointerGetDatum(&fc_req)));

		if (fc_sresult == &fc_req)
		{
			/* Success */
			ReleaseSysCache(fc_proctup);
			return fc_req.rows;
		}
	}

	/* 没有支持函数，或它失败了，因此依赖于 prorows */
	fc_result = fc_procform->prorows;

	ReleaseSysCache(fc_proctup);

	return fc_result;
}

/*
 * has_unique_index
 *
 * 检测指定关系的指定属性是否有唯一索引，从而使我们能够得出该属性的所有（非空）值都是唯一的结论。
 *
 * 此功能不检查索引的 indimmediate 属性，这意味着唯一性可能在事务内短暂失效。
 * 当我们进行统计估计时，这是适当的，但在进行任何正确性证明时要当心使用它。
 */
bool has_unique_index(RelOptInfo *fc_rel, AttrNumber fc_attno)
{
	ListCell   *fc_ilist;

	foreach(fc_ilist, fc_rel->indexlist)
	{
		IndexOptInfo *fc_index = (IndexOptInfo *) lfirst(fc_ilist);

		/*
		 * 注意：忽略部分索引，因为它们不允许我们得出所有 attr 值都是唯一的结论，*除非* 它们被标记为 predOK
		 * 这意味着我们知道索引的谓词被查询满足。我们也不关心表达式索引。
		 * 此外，多列唯一索引并不允许我们得出仅指定的属性是唯一的结论。
		 */
		if (fc_index->unique &&
			fc_index->nkeycolumns == 1 &&
			fc_index->indexkeys[0] == fc_attno &&
			(fc_index->indpred == NIL || fc_index->predOK))
			return true;
	}
	return false;
}


/*
 * has_row_triggers
 *
 * 检测指定关系是否针对事件具有任何行级触发器。
 */
bool has_row_triggers(PlannerInfo *fc_root, Index fc_rti, CmdType fc_event)
{
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_rti, fc_root);
	Relation	fc_relation;
	TriggerDesc *fc_trigDesc;
	bool		fc_result = false;

	/* 假设我们已经拥有足够的锁 */
	fc_relation = table_open(fc_rte->relid, NoLock);

	fc_trigDesc = fc_relation->trigdesc;
	switch (fc_event)
	{
		case CMD_INSERT:
			if (fc_trigDesc &&
				(fc_trigDesc->trig_insert_after_row ||
				 fc_trigDesc->trig_insert_before_row))
				fc_result = true;
			break;
		case CMD_UPDATE:
			if (fc_trigDesc &&
				(fc_trigDesc->trig_update_after_row ||
				 fc_trigDesc->trig_update_before_row))
				fc_result = true;
			break;
		case CMD_DELETE:
			if (fc_trigDesc &&
				(fc_trigDesc->trig_delete_after_row ||
				 fc_trigDesc->trig_delete_before_row))
				fc_result = true;
			break;
			/* 合并没有单独的事件，只有插入/更新/删除 */
		case CMD_MERGE:
			fc_result = false;
			break;
		default:
			elog(ERROR, "unrecognized CmdType: %d", (int) fc_event);
			break;
	}

	table_close(fc_relation, NoLock);
	return fc_result;
}

/*
 * has_stored_generated_columns
 *
 * 由 RTI 标识的表是否具有任何存储生成的列？
 */
bool has_stored_generated_columns(PlannerInfo *fc_root, Index fc_rti)
{
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_rti, fc_root);
	Relation	fc_relation;
	TupleDesc	fc_tupdesc;
	bool		fc_result = false;

	/* 假设我们已经拥有足够的锁 */
	fc_relation = table_open(fc_rte->relid, NoLock);

	fc_tupdesc = RelationGetDescr(fc_relation);
	fc_result = fc_tupdesc->constr && fc_tupdesc->constr->has_generated_stored;

	table_close(fc_relation, NoLock);

	return fc_result;
}

/*
 * get_dependent_generated_columns
 *
 * 获取依赖于 target_cols 中列的任何存储生成列的列号。
 * 输入和结果位图集都包含列号，列号偏移为 FirstLowInvalidHeapAttributeNumber。
 */
Bitmapset * get_dependent_generated_columns(PlannerInfo *fc_root, Index fc_rti,
								Bitmapset *fc_target_cols)
{
	Bitmapset  *fc_dependentCols = NULL;
	RangeTblEntry *fc_rte = planner_rt_fetch(fc_rti, fc_root);
	Relation	fc_relation;
	TupleDesc	fc_tupdesc;
	TupleConstr *fc_constr;

	/* 假设我们已经拥有足够的锁 */
	fc_relation = table_open(fc_rte->relid, NoLock);

	fc_tupdesc = RelationGetDescr(fc_relation);
	fc_constr = fc_tupdesc->constr;

	if (fc_constr && fc_constr->has_generated_stored)
	{
		for (int fc_i = 0; fc_i < fc_constr->num_defval; fc_i++)
		{
			AttrDefault *fc_defval = &fc_constr->defval[fc_i];
			Node	   *fc_expr;
			Bitmapset  *fc_attrs_used = NULL;

			/* 如果不是生成列则跳过 */
			if (!TupleDescAttr(fc_tupdesc, fc_defval->adnum - 1)->attgenerated)
				continue;

			/* 确定此生成列依赖的列 */
			fc_expr = stringToNode(fc_defval->adbin);
			pull_varattnos(fc_expr, 1, &fc_attrs_used);

			if (bms_overlap(fc_target_cols, fc_attrs_used))
				fc_dependentCols = bms_add_member(fc_dependentCols,
											   fc_defval->adnum - FirstLowInvalidHeapAttributeNumber);
		}
	}

	table_close(fc_relation, NoLock);

	return fc_dependentCols;
}


/***************************************
 * set_relation_partition_info
 *
 * 为分区表设置分区方案及相关信息。
 ***************************************/
static void set_relation_partition_info(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							Relation fc_relation)
{
	PartitionDesc fc_partdesc;

	/***************************************
	 * 如果未创建 PartitionDirectory 基础结构，则创建之。
 ***************************************/
	if (fc_root->glob->partition_directory == NULL)
	{
		fc_root->glob->partition_directory =
			CreatePartitionDirectory(CurrentMemoryContext, true);
	}

	fc_partdesc = PartitionDirectoryLookup(fc_root->glob->partition_directory,
										fc_relation);
	fc_rel->part_scheme = fc_find_partition_scheme(fc_root, fc_relation);
	Assert(fc_partdesc != NULL && fc_rel->part_scheme != NULL);
	fc_rel->boundinfo = fc_partdesc->boundinfo;
	fc_rel->nparts = fc_partdesc->nparts;
	set_baserel_partition_key_exprs(fc_relation, fc_rel);
	set_baserel_partition_constraint(fc_relation, fc_rel);
}

/*
 * find_partition_scheme
 *
 * 查找或创建此关系的 PartitionScheme。
 */
static PartitionScheme
fc_find_partition_scheme(PlannerInfo *fc_root, Relation fc_relation)
{
	PartitionKey fc_partkey = RelationGetPartitionKey(fc_relation);
	ListCell   *fc_lc;
	int			fc_partnatts,
				fc_i;
	PartitionScheme fc_part_scheme;

	/* 分区表应该有一个分区键。 */
	Assert(fc_partkey != NULL);

	fc_partnatts = fc_partkey->partnatts;

	/* 搜索匹配的分区方案并返回如果找到一个。 */
	foreach(fc_lc, fc_root->part_schemes)
	{
		fc_part_scheme = lfirst(fc_lc);

		/* 匹配分区策略和键的数量。 */
		if (fc_partkey->strategy != fc_part_scheme->strategy ||
			fc_partnatts != fc_part_scheme->partnatts)
			continue;

		/* 匹配分区键类型属性。 */
		if (memcmp(fc_partkey->partopfamily, fc_part_scheme->partopfamily,
				   sizeof(Oid) * fc_partnatts) != 0 ||
			memcmp(fc_partkey->partopcintype, fc_part_scheme->partopcintype,
				   sizeof(Oid) * fc_partnatts) != 0 ||
			memcmp(fc_partkey->partcollation, fc_part_scheme->partcollation,
				   sizeof(Oid) * fc_partnatts) != 0)
			continue;

		/*
		 * 当partopcintype匹配时，长度和byval信息应匹配。
		 */
		Assert(memcmp(fc_partkey->parttyplen, fc_part_scheme->parttyplen,
					  sizeof(int16) * fc_partnatts) == 0);
		Assert(memcmp(fc_partkey->parttypbyval, fc_part_scheme->parttypbyval,
					  sizeof(bool) * fc_partnatts) == 0);

		/*
		 * 如果partopfamily和partopcintype匹配，则必须具有相同的
		 * 分区比较函数。请注意，我们无法可靠地
		 * 断言函数结构本身的相等性，因为它们可能在
		 * PartitionKey之间是不同的，所以只需断言函数
		 * OID。
		 */
#ifdef USE_ASSERT_CHECKING
		for (fc_i = 0; fc_i < fc_partkey->partnatts; fc_i++)
			Assert(fc_partkey->partsupfunc[fc_i].fn_oid ==
				   fc_part_scheme->partsupfunc[fc_i].fn_oid);
#endif

		/* 找到匹配的分区方案。 */
		return fc_part_scheme;
	}

	/*
	 * 未找到匹配的分区方案。创建一个，从relcache复制相关
	 * 信息。我们需要复制数组的内容，因为relcache项可能
	 * 在我们关闭关系后不会存活。
	 */
	fc_part_scheme = (PartitionScheme) palloc0(sizeof(PartitionSchemeData));
	fc_part_scheme->strategy = fc_partkey->strategy;
	fc_part_scheme->partnatts = fc_partkey->partnatts;

	fc_part_scheme->partopfamily = (Oid *) palloc(sizeof(Oid) * fc_partnatts);
	memcpy(fc_part_scheme->partopfamily, fc_partkey->partopfamily,
		   sizeof(Oid) * fc_partnatts);

	fc_part_scheme->partopcintype = (Oid *) palloc(sizeof(Oid) * fc_partnatts);
	memcpy(fc_part_scheme->partopcintype, fc_partkey->partopcintype,
		   sizeof(Oid) * fc_partnatts);

	fc_part_scheme->partcollation = (Oid *) palloc(sizeof(Oid) * fc_partnatts);
	memcpy(fc_part_scheme->partcollation, fc_partkey->partcollation,
		   sizeof(Oid) * fc_partnatts);

	fc_part_scheme->parttyplen = (int16 *) palloc(sizeof(int16) * fc_partnatts);
	memcpy(fc_part_scheme->parttyplen, fc_partkey->parttyplen,
		   sizeof(int16) * fc_partnatts);

	fc_part_scheme->parttypbyval = (bool *) palloc(sizeof(bool) * fc_partnatts);
	memcpy(fc_part_scheme->parttypbyval, fc_partkey->parttypbyval,
		   sizeof(bool) * fc_partnatts);

	fc_part_scheme->partsupfunc = (FmgrInfo *)
		palloc(sizeof(FmgrInfo) * fc_partnatts);
	for (fc_i = 0; fc_i < fc_partnatts; fc_i++)
		fmgr_info_copy(&fc_part_scheme->partsupfunc[fc_i], &fc_partkey->partsupfunc[fc_i],
					   CurrentMemoryContext);

	/* 将分区方案添加到PlannerInfo。 */
	fc_root->part_schemes = lappend(fc_root->part_schemes, fc_part_scheme);

	return fc_part_scheme;
}

/*
 * set_baserel_partition_key_exprs
 *
 * 为给定的基本关系构建分区键表达式，并填充
 * rel->partexprs。
 */
static void set_baserel_partition_key_exprs(Relation fc_relation,
								RelOptInfo *fc_rel)
{
	PartitionKey fc_partkey = RelationGetPartitionKey(fc_relation);
	int			fc_partnatts;
	int			fc_cnt;
	List	  **fc_partexprs;
	ListCell   *fc_lc;
	Index		fc_varno = fc_rel->relid;

	Assert(IS_SIMPLE_REL(fc_rel) && fc_rel->relid > 0);

	/* 分区表应该有一个分区键。 */
	Assert(fc_partkey != NULL);

	fc_partnatts = fc_partkey->partnatts;
	fc_partexprs = (List **) palloc(sizeof(List *) * fc_partnatts);
	fc_lc = list_head(fc_partkey->partexprs);

	for (fc_cnt = 0; fc_cnt < fc_partnatts; fc_cnt++)
	{
		Expr	   *fc_partexpr;
		AttrNumber	fc_attno = fc_partkey->partattrs[fc_cnt];

		if (fc_attno != InvalidAttrNumber)
		{
			/* 单列分区键存储为Var节点。 */
			Assert(fc_attno > 0);

			fc_partexpr = (Expr *) makeVar(fc_varno, fc_attno,
										fc_partkey->parttypid[fc_cnt],
										fc_partkey->parttypmod[fc_cnt],
										fc_partkey->parttypcoll[fc_cnt], 0);
		}
		else
		{
			if (fc_lc == NULL)
				elog(ERROR, "wrong number of partition key expressions");

			/* 用给定的varno重新标记表达式。 */
			fc_partexpr = (Expr *) copyObject(lfirst(fc_lc));
			ChangeVarNodes((Node *) fc_partexpr, 1, fc_varno, 0);
			fc_lc = lnext(fc_partkey->partexprs, fc_lc);
		}

		/* 基本关系每个键有一个表达式。 */
		fc_partexprs[fc_cnt] = list_make1(fc_partexpr);
	}

	fc_rel->partexprs = fc_partexprs;

	/*
	 * 基本关系没有可为空的分区键表达式，因为
	 * 没有外连接。我们仍然分配一个空表达式列表的数组，
	 * 以保持分区键表达式处理代码简单。
	 * 请参见build_joinrel_partition_info()和match_expr_to_partition_keys()。
	 */
	fc_rel->nullable_partexprs = (List **) palloc0(sizeof(List *) * fc_partnatts);
}

/*
 * set_baserel_partition_constraint
 *
 * 为给定的基本关系构建分区约束并将其设置在
 * 给定的RelOptInfo中。所有Var节点都用给定关系的relid重新标记。
 */
static void set_baserel_partition_constraint(Relation fc_relation, RelOptInfo *fc_rel)
{
	List	   *fc_partconstr;

	if (fc_rel->partition_qual)	/* 已经完成 */
		return;

	/*
	 * 通过类似于检查约束的const-simplification来运行
	 * 分区条件。我们跳过canonicalize_qual，因为
	 * 分区条件应该已经是规范形式；此外，由于条件处于
	 * 隐式与格式，我们将不得不显式地将其转换为显式
	 * 与格式，然后再转换回来。
	 */
	fc_partconstr = RelationGetPartitionQual(fc_relation);
	if (fc_partconstr)
	{
		fc_partconstr = (List *) expression_planner((Expr *) fc_partconstr);
		if (fc_rel->relid != 1)
			ChangeVarNodes((Node *) fc_partconstr, 1, fc_rel->relid, 0);
		fc_rel->partition_qual = fc_partconstr;
	}
}
